/// ****************************************** ///
/// Altered with permission from the excellent ///
/// MVC contrib project http://mvccontrib.org  ///
/// ****************************************** ///
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Web.Mvc;

namespace Mvc.XForms.Utilities {
	/// <summary>
	/// Provides the ability to deserialize a Request into a CLR object.
	/// </summary>
	public abstract class NameValueDeserializer {
		private ModelStateDictionary modelState;

		public NameValueDeserializer(ModelStateDictionary modelState) {
			this.modelState = modelState;
		}

		// This is required to enable deserialization of relationships where the child
		// is of type IEntity. If the ID of the entity is in the request, this method
		// will be called to load the object and other request properties will be
		// updated otherwise a new one will be created (via the empty constructor).
		// Dependency injection is the best method to "forward" this method to the
		// appropriate service.
		protected abstract object GetEntity(long entityID, PropertyInfo property);

		protected virtual IConvertible GetConvertible(string sValue) {
			return new DefaultConvertible(sValue);
		}

		/// <summary>
		/// Deserializes the specified request collection.
		/// </summary>
		/// <typeparam name="T">The type to deserialize to.</typeparam>
		/// <param name="collection">The collection.</param>
		/// <param name="prefix">The prefix.</param>
		/// <returns>The deserialized object</returns>
		public void Deserialize(object instance, NameValueCollection collection) {
			Deserialize(collection, null, instance.GetType(), ref instance);
		}

		/// <summary>
		/// Deserializes the specified request collection.
		/// </summary>
		/// <typeparam name="T">The type to deserialize to.</typeparam>
		/// <param name="collection">The collection.</param>
		/// <param name="prefix">The prefix.</param>
		/// <returns>The deserialized object</returns>
		public void Deserialize(object instance, NameValueCollection collection, string prefix) {
			Deserialize(collection, prefix, instance.GetType(), ref instance);
		}

		/// <summary>
		/// Deserializes the specified request collection.
		/// </summary>
		/// <param name="collection">The collection.</param>
		/// <param name="prefix">The prefix.</param>
		/// <param name="targetType">Type of the target.</param>
		/// <returns>The deserialized object</returns>
		public object Deserialize(NameValueCollection collection, string prefix, Type targetType) {
			if (collection == null || collection.Count == 0) return null;

			if (prefix == string.Empty) throw new ArgumentException("prefix must not be empty", prefix);

			if (targetType == null) throw new ArgumentNullException("targetType");

			if (targetType.IsArray) {
				Type elementType = targetType.GetElementType();
				ArrayList arrayInstance = DeserializeArrayList(collection, prefix, elementType);
				return arrayInstance.ToArray(elementType);
			}

			if (IsGenericList(targetType)) {
				IList genericListInstance = CreateGenericListInstance(targetType);
				DeserializeGenericList(collection, prefix, targetType, genericListInstance);
				return genericListInstance;
			}

			object instance = null;
			Deserialize(collection, prefix, targetType, ref instance);
			return instance ?? CreateInstance(targetType);
		}

		protected virtual void Deserialize(NameValueCollection collection, string prefix, Type targetType, ref object instance) {
			if (CheckPrefixInRequest(collection, prefix)) {
				if (instance == null) {
					instance = CreateInstance(targetType);
				}

				PropertyInfo[] properties = GetProperties(targetType);

				foreach (var property in properties) {
					string name = prefix != null ? string.Concat(prefix, ExpressionHelper.Separator, property.Name) : property.Name;

					if (CheckPrefixInRequest(collection, name)) {
						Type propertyType = property.PropertyType;

						if (IsSimpleProperty(propertyType)) {
							string sValue = collection.Get(name);
							if (sValue != null) {
								SetValue(instance, property, GetConvertible(sValue), name);
							}
						} else if (propertyType.IsArray) {
							Type elementType = propertyType.GetElementType();

							ArrayList arrayInstance = DeserializeArrayList(collection, name, elementType);

							SetValue(instance, property, arrayInstance.ToArray(elementType));
						} else if (IsGenericList(propertyType)) {
							IList genericListProperty = GetGenericListProperty(instance, property);

							if (genericListProperty == null) continue;

							DeserializeGenericList(collection, name, propertyType, genericListProperty);
						} else {
							object complexProperty = GetComplexProperty(collection, name, instance, property);

							if (complexProperty == null) continue;

							Deserialize(collection, name, propertyType, ref complexProperty);
						}
					}
				}
			}
		}

		protected virtual void DeserializeGenericList(NameValueCollection collection, string prefix, Type targetType, IList instance) {
			instance.Clear();
			Type elementType = targetType.GetGenericArguments()[0];

			ArrayList arrayInstance = DeserializeArrayList(collection, prefix, elementType);

			foreach (var inst in arrayInstance) {
				instance.Add(inst);
			}
		}

		protected virtual IList GetGenericListProperty(object instance, PropertyInfo property) {
			// If property is already initialized (via object's constructor) use that
			// Otherwise attempt to new it
			var genericListProperty = property.GetValue(instance, null) as IList;
			if (genericListProperty == null) {
				genericListProperty = CreateGenericListInstance(property.PropertyType);
				if (!SetValue(instance, property, genericListProperty)) {
					return null;
				}
			}
			return genericListProperty;
		}

		protected virtual IList CreateGenericListInstance(Type targetType) {
			Type elementType = targetType.GetGenericArguments()[0];

			Type desiredListType = targetType.IsInterface
									? typeof(List<>).MakeGenericType(elementType)
									: targetType;

			return CreateInstance(desiredListType) as IList;
		}

		protected virtual bool IsGenericList(Type instanceType) {
			if (!instanceType.IsGenericType) {
				return false;
			}

			if (typeof(IList).IsAssignableFrom(instanceType)) {
				return true;
			}

			Type[] genericArgs = instanceType.GetGenericArguments();

			Type listType = typeof(IList<>).MakeGenericType(genericArgs[0]);

			return listType.IsAssignableFrom(instanceType);
		}

		protected virtual ArrayList DeserializeArrayList(NameValueCollection collection, string prefix, Type elementType) {
			// Collection is a multiselect list (eg Orders, Orders, Orders), possibly a "Select" control
			var values = collection.GetValues(prefix);
			if (values != null) {
				// First one in the list **SHOULD** is a dummy value so we know the list exists, otherwise
				// when no items are selected, nothing would be in the request. The value will be the
				// valueSelector id, so we can populate complex list objects.
				var valueSelector = values[0];
				var formCollection = new FormCollection();
				var itemPrefixFormat = valueSelector.Length > 0 ?
					string.Concat(prefix, ExpressionHelper.IndexSeparator, "{0}", ExpressionHelper.Separator, valueSelector) : string.Concat(prefix, "-{0}");

				values.Skip(1).ForEach((v, i) => {
					var itemPrefix = string.Format(itemPrefixFormat, i);
					formCollection.Add(itemPrefix, v);
				});

				return CreateArrayList(formCollection, prefix, elementType);
			}

			// Collection is an indexed list (eg Orders-0, Orders-1, Orders-2), possibly a "Repeater" control		
			return CreateArrayList(collection, prefix, elementType);
		}

		private ArrayList CreateArrayList(NameValueCollection collection, string prefix, Type elementType) {
			string[] arrayPrefixes = GetArrayPrefixes(collection, prefix);
			var arrayInstance = new ArrayList(arrayPrefixes.Length);

			arrayPrefixes.ForEach(arrayPrefix => {
				object inst = null;

				if (IsSimpleProperty(elementType)) {
					string sValue = collection.Get(arrayPrefix);
					if (sValue != null) {
						//string sValue = string.Empty;
						//if (TryGetValue(collection, arrayPrefix, out sValue))
						//{
						inst = GetConvertible(sValue).ToType(elementType, CultureInfo.CurrentCulture);
					}
				} else {
					inst = Deserialize(collection, arrayPrefix, elementType);
				}

				if (inst != null) {
					arrayInstance.Add(inst);
				}
			});

			return arrayInstance;
		}

		private bool TryGetValue(NameValueCollection item, string key, out string value) {
			value = item.Get(key);
			if (!string.IsNullOrEmpty(value)) {
				return true;
			}
			List<string> keys = new List<string>(item.AllKeys);
			foreach (string k in keys) {
				if (k.ToLower().Split(ExpressionHelper.Separator)[0] == key.ToLower()) {
					value = item.Get(k);
					return true;
				}
			}
			value = string.Empty;
			return false;
		}

		protected virtual bool CheckPrefixInRequest(NameValueCollection collection, string prefix) {
			if (prefix != null)
				return collection.AllKeys.Any(key => key != null && key.StartsWith(prefix, true, CultureInfo.InvariantCulture));
			return true;
		}

		protected virtual string[] GetArrayPrefixes(NameValueCollection collection, string prefix) {
			var arrayPrefixes = new List<string>();

			prefix = string.Concat(prefix, ExpressionHelper.IndexSeparator).ToLower();
			int prefixLength = prefix.Length;
			string[] names = collection.AllKeys;
			foreach (string name in names) {
				if (name.IndexOf(prefix, StringComparison.InvariantCultureIgnoreCase) == 0) {
					int bracketIndex = name.IndexOf(ExpressionHelper.Separator, prefixLength);
					if (bracketIndex > prefixLength) {
						string arrayPrefix = name.Substring(0, bracketIndex).ToLower();
						if (!arrayPrefixes.Contains(arrayPrefix)) {
							arrayPrefixes.Add(arrayPrefix);
						}
					} else if (bracketIndex == -1) {
						arrayPrefixes.Add(name);
					}
				}
			}

			return arrayPrefixes.ToArray();
		}

		private static readonly Dictionary<Type, PropertyInfo[]> _cachedProperties = new Dictionary<Type, PropertyInfo[]>();

		private static readonly object _syncRoot = new object();

		protected static PropertyInfo[] GetProperties(Type targetType) {
			PropertyInfo[] properties;
			if (!_cachedProperties.TryGetValue(targetType, out properties)) {
				lock (_syncRoot) {
					if (!_cachedProperties.TryGetValue(targetType, out properties)) {
						properties = targetType.GetProperties();
						_cachedProperties.Add(targetType, properties);
					}
				}
			}
			return properties;
		}

		protected virtual bool SetValue(object instance, PropertyInfo property, object value) {
			if (property.CanWrite) {
				property.SetValue(instance, value, null);
				return true;
			}

			return false;
		}

		protected virtual bool SetValue(object instance, PropertyInfo property, IConvertible oValue, string requestKey) {
			try {
				object convertedValue = oValue.ToType(property.PropertyType, CultureInfo.CurrentCulture);
				return SetValue(instance, property, convertedValue);
			}
			catch {
				modelState.AddModelError(requestKey, "Invalid field");
				return false;
			}
		}

		protected virtual object CreateInstance(Type targetType) {
			return Activator.CreateInstance(targetType);
		}

		protected virtual object GetComplexProperty(NameValueCollection collection, string prefix, object instance, PropertyInfo property) {
			object complexProperty = property.GetValue(instance, null);
			if (complexProperty == null) {
				complexProperty = CreateInstance(property.PropertyType);
				if (!SetValue(instance, property, complexProperty)) {
					return null;
				}
			}

			var entity = complexProperty as IEntity;
			if (entity != null) {
				var idField = collection[prefix + "_ID"];
				if (idField != null) {
					var entityID = Convert.ToInt64(idField);
					if (entityID > 0)
						complexProperty = GetEntity(entityID, property);

					if (!SetValue(instance, property, complexProperty))
						return null;
				}
			}
			return complexProperty;
		}

		protected virtual bool IsSimpleProperty(Type propertyType) {
			if (propertyType.IsArray) {
				return false;
			}

			bool isSimple = propertyType.IsPrimitive ||
							propertyType.IsEnum ||
							propertyType == typeof(String) ||
							propertyType == typeof(Guid) ||
							propertyType == typeof(DateTime) ||
							propertyType == typeof(Decimal);

			if (isSimple) {
				return true;
			}

			TypeConverter tconverter = TypeDescriptor.GetConverter(propertyType);

			return tconverter.CanConvertFrom(typeof(String));
		}
	}
}